package moa.cluster;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import moa.gui.visualization.DataPoint;
import weka.core.DenseInstance;
import weka.core.Instance;

/**
 * A simple implementation of the <code>Cluster</code> interface representing
 * spherical clusters. The inclusion probability is one inside the sphere and zero
 * everywhere else.
 *
 */
public class SphereCluster extends Cluster {

    private double[] center;
    private double radius;
    private double weight;


    public SphereCluster(double[] center, double radius) {
	this( center, radius, 1.0 );
    }

    public SphereCluster() {
    }

    public SphereCluster( double[] center, double radius, double weightedSize) {
        this();
        this.center = center;
        this.radius = radius;
	this.weight = weightedSize;
    }

    public SphereCluster(int dimensions, double radius, Random random) {
        this();
        this.center = new double[dimensions];
        this.radius = radius;

        // Position randomly but keep hypersphere inside the boundaries
        double interval = 1.0 - 2 * radius;
        for (int i = 0; i < center.length; i++) {
            this.center[i] = (random.nextDouble() * interval) + radius;
        }
        this.weight = 0.0;
    }


    public SphereCluster(List<?extends Instance> instances, int dimension){
        this();
        if(instances == null || instances.size() <= 0)
            return;

        weight = instances.size();

        Miniball mb = new Miniball(dimension);
        mb.clear();

        for (Instance instance : instances) {
            mb.check_in(instance.toDoubleArray());
        }

        mb.build();
        center = mb.center();
        radius = mb.radius();
        mb.clear();
    }


    /**
     * Checks whether two <code>SphereCluster</code> overlap based on radius
     * NOTE: overlapRadiusDegree only calculates the overlap based
     * on the centers and the radi, so not the real overlap
     *
     * TODO: should we do this by MC to get the real overlap???
     *
     * @param other
     * @return
     */
    
    public double overlapRadiusDegree(SphereCluster other) {
        

        double[] center0 = getCenter();
        double radius0 = getRadius();

        double[] center1 = other.getCenter();
        double radius1 = other.getRadius();
        
        double radiusBig;
        double radiusSmall;
        if(radius0 < radius1){
            radiusBig = radius1;
            radiusSmall = radius0;
        }
        else{
            radiusBig = radius0;
            radiusSmall = radius1;
        }
            
        double dist = 0;
        for (int i = 0; i < center0.length; i++) {
            double delta = center0[i] - center1[i];
            dist += delta * delta;
        }
        dist = Math.sqrt(dist);
        
        if(dist > radiusSmall + radiusBig)
               return 0;
        if(dist + radiusSmall <= radiusBig){
            //one lies within the other
            return 1;
        }
        else{
            return (radiusSmall+radiusBig-dist)/(2*radiusSmall);
        }
    }

    public void combine(SphereCluster cluster) {
        //double max_radius = distance(center, cluster.getCenter()) + radius + cluster.getRadius();
        double[] center = getCenter();
        double[] newcenter = new double[center.length];
        double[] other_center = cluster.getCenter();
        double other_weight = cluster.getWeight();
        double other_radius = cluster.getRadius();

        for (int i = 0; i < center.length; i++) {
            newcenter[i] = (center[i]*getWeight()+other_center[i]*other_weight)/(getWeight()+other_weight);
        }

        //double overlap = overlapDegree(cluster);
        center = newcenter;
        double r_0 = getRadius() + Math.abs(distance(center, newcenter));
        double r_1 = other_radius + Math.abs(distance(other_center, newcenter));
        radius = Math.max(r_0, r_1);
        weight+= other_weight;
    }

    public void merge(SphereCluster cluster) {
        double[] c0 = getCenter();
        double w0 = getWeight();
        double r0 = getRadius();

        double[] c1 = cluster.getCenter();
        double w1 = cluster.getWeight();
        double r1 = cluster.getRadius();

        //vector
        double[] v = new double[c0.length];
        //center distance
        double d = 0;

        for (int i = 0; i < c0.length; i++) {
            v[i] = c0[i] - c1[i];
            d += v[i] * v[i];
        }
        d = Math.sqrt(d);



        double r = 0;
        double[] c = new double[c0.length];

        //one lays within the others
        if(d + r0 <= r1  || d + r1 <= r0){
            if(d + r0 <= r1){
                r = r1;
                c = c1;
            }
            else{
                r = r0;
                c = c0;
            }
        }
        else{
            r = (r0 + r1 + d)/2.0;
            for (int i = 0; i < c.length; i++) {
                c[i] = c1[i] - v[i]/d * (r1-r);
            }
        }

        setCenter(c);
        setRadius(r);
        setWeight(w0+w1);



    }

    @Override
    public double[] getCenter() {
        double[] copy = new double[center.length];
        System.arraycopy(center, 0, copy, 0, center.length);
        return copy;
    }

    public void setCenter(double[] center) {
        this.center = center;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius( double radius ) {
	this.radius = radius;
    }
    
    @Override
    public double getWeight() {
	return weight;
    }

    public void setWeight( double weight ) {
	this.weight = weight;
    }

    @Override
    public double getInclusionProbability(Instance instance) {
        if (getCenterDistance(instance) <= getRadius()) {
            return 1.0;
        }
        return 0.0;
    }

    public double getCenterDistance(Instance instance) {
        double distance = 0.0;
        //get the center through getCenter so subclass have a chance
        double[] center = getCenter();
        for (int i = 0; i < center.length; i++) {
            double d = center[i] - instance.value(i);
            distance += d * d;
        }
        return Math.sqrt(distance);
    }

    public double getCenterDistance(SphereCluster other) {
        return distance(getCenter(), other.getCenter());
    }

    /*
     * the minimal distance between the surface of two clusters.
     * is negative if the two clusters overlap
     */
    public double getHullDistance(SphereCluster other) {
        double distance = 0.0;
        //get the center through getCenter so subclass have a chance
        double[] center0 = getCenter();
        double[] center1 = other.getCenter();
        distance = distance(center0, center1);

        distance = distance - getRadius() - other.getRadius();
        return distance;
    }

    /*
     * When a clusters looses points the new minimal bounding sphere can be
     * partly outside of the originating cluster. If a another cluster is
     * right next to the original cluster (without overlapping), the new
     * cluster can be overlapping with this second cluster. OverlapSave
     * will tell you if the current cluster can degenerate so much that it
     * overlaps with cluster 'other'
     */

    public boolean overlapSave(SphereCluster other){
        //use basic geometry to figure out the maximal degenerated cluster
        //comes down to Max(radius *(sin alpha + cos alpha)) which is
        double minDist = Math.sqrt(2)*(getRadius() + other.getRadius());
        double diff = getCenterDistance(other) - minDist;

//        double maxRadius = Math.max(getRadius(),other.getRadius());
//        double minDist = (Math.sqrt(2)-1)*maxRadius;
//        double diff = getHullDistance(other) - minDist;

        if(diff > 0)
            return true;
        else
            return false;
    }

    private double distance(double[] v1, double[] v2){
        double distance = 0.0;
        double[] center = getCenter();
        for (int i = 0; i < center.length; i++) {
            double d = v1[i] - v2[i];
            distance += d * d;
        }
        return Math.sqrt(distance);
    }

    public double[] getDistanceVector(Instance instance){
        return distanceVector(getCenter(), instance.toDoubleArray());
    }

    public double[] getDistanceVector(SphereCluster other){
        return distanceVector(getCenter(), other.getCenter());
    }

    private double[] distanceVector(double[] v1, double[] v2){
        double[] v = new double[v1.length];
        for (int i = 0; i < v1.length; i++) {
            v[i] = v2[i] - v1[i];
        }
        return v;
    }


        /**
     * Samples this cluster by returning a point from inside it.
     * @param random a random number source
     * @return a point that lies inside this cluster
     */
    public Instance sample(Random random) {
        // Create sample in hypersphere coordinates

        //get the center through getCenter so subclass have a chance
        double[] center = getCenter();

        final int dimensions = center.length;

	final double sin[] = new double[dimensions - 1];
	final double cos[] = new double[dimensions - 1];
        final double length = random.nextDouble() * getRadius();

	double lastValue = 1.0;
        for (int i = 0; i < dimensions-1; i++) {
            double angle = random.nextDouble() * 2 * Math.PI;
	    sin[i] = lastValue * Math.sin( angle ); // Store cumulative values
	    cos[i] = Math.cos( angle );
	    lastValue = sin[i];
        }

	// Calculate cartesian coordinates
	double res[] = new double[dimensions];

	// First value uses only cosines
	res[0] = center[0] + length*cos[0];

	// Loop through 'middle' coordinates which use cosines and sines
        for (int i = 1; i < dimensions-1; i++) {
            res[i] = center[i] + length*sin[i-1]*cos[i];
        }

	// Last value uses only sines
	res[dimensions-1] = center[dimensions-1] + length*sin[dimensions-2];

        return new DenseInstance(1.0, res);
    }

    @Override
    protected void getClusterSpecificInfo(ArrayList<String> infoTitle, ArrayList<String> infoValue) {
        super.getClusterSpecificInfo(infoTitle, infoValue);
        infoTitle.add("Radius");
        infoValue.add(Double.toString(getRadius()));
    }


}
